﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Globalization;

namespace C4F.DevKit.Calendar.Parser
{
    /// <summary>
    /// A class used to read in iCal files.
    /// </summary>
    public class Reader
    {
        #region Private Members

        private StreamReader _streamReader;

        private int _utcOffset;

        private bool _timezoneExists;

        #endregion

        #region Public Properties

        /// <summary>
        /// The filestream attached to the iCal file to be used.
        /// </summary>
        public FileStream FileStream
        {
            set
            {
                _streamReader = new StreamReader(value);
            }
        }

        /// <summary>
        /// The path of the iCal file to be used.
        /// </summary>
        public string FilePath
        {
            set
            {
                _streamReader = new StreamReader(value);
            }
        }

        #endregion

        #region Public Constructors

        /// <summary>
        /// Creates a new calendar reader using the file specified.
        /// </summary>
        /// <param name="filepath">The iCal file to read.</param>
        public Reader(string filepath)
        {
            _streamReader = new StreamReader(filepath);
        }

        /// <summary>
        /// Creates a new calendar reader using the StreamReader specified.
        /// </summary>
        /// <param name="streamReader">The StreamReader from which to read the iCal file.</param>
        public Reader(StreamReader streamReader)
        {
            _streamReader = streamReader;
        }

        /// <summary>
        /// Creates a new calendar reader using the FileStream specified.
        /// </summary>
        /// <param name="fileStream">The FileStream from which to read the iCal file.</param>
        public Reader(FileStream fileStream)
        {
            _streamReader = new StreamReader(fileStream);
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Reads an iCal file.
        /// </summary>
        /// <returns>A list of appointments from the calendar.</returns>
        public IEnumerable<Appointment> ReadCalendar()
        {
            FindCalendarStart();

            List<Appointment> apptList = new List<Appointment>();
            _timezoneExists = MoveToTimeZone();

            if (_timezoneExists)
            {
                GetUTCOffset();
                MoveToNextEvent();
            }            

            while (!_streamReader.EndOfStream)
            {
                Appointment appt = new Appointment();
                string currentLine = GetNextLine();

                while (!currentLine.EndsWith(CalendarConstants.END_APPOINTMENT))
                {
                    ProcessLine(currentLine, appt);
                    currentLine = GetNextLine();
                }

                apptList.Add(appt);
                MoveToNextEvent();
            }

            CombineOccurrences(apptList);
            _streamReader.Close();

            return apptList;
        }

        #endregion

        #region Public Static Methods

        /// <summary>
        /// Reads an iCal file.
        /// </summary>
        /// <param name="filepath">The path of the iCal file to read.</param>
        /// <returns>A list of appointments from the calendar.</returns>
        public static IEnumerable<Appointment> ReadCalendar(string filepath)
        {
            Reader reader = new Reader(filepath);
            return reader.ReadCalendar();
        }

        /// <summary>
        /// Reads an iCal file.
        /// </summary>
        /// <param name="fileStream">The filestream attached to the iCal file to be read.</param>
        /// <returns>A list of appointments from the calendar.</returns>
        public static IEnumerable<Appointment> ReadCalendar(FileStream fileStream)
        {
            Reader reader = new Reader(fileStream);
            return reader.ReadCalendar();
        }

        #endregion

        #region Private Methods

        private void FindCalendarStart()
        {
            string currentLine = GetNextLine();

            while (!currentLine.StartsWith(CalendarConstants.BEGIN_CALENDAR) && !_streamReader.EndOfStream)
            {
                currentLine = GetNextLine();
            }

            if (_streamReader.EndOfStream)
            {
                throw new CalendarException(string.Format("Invalid calendar file: missing {0}", CalendarConstants.BEGIN_CALENDAR.ToString()));
            }
        }

        /// <summary>
        /// Moves to the timezone component within this iCal file, if it exists.
        /// Otherwise, moves to the next event.
        /// </summary>
        /// <returns>True if the timezone component was found, false otherwise.</returns>
        private bool MoveToTimeZone()
        {
            string currentLine = _streamReader.ReadLine();

            while (!(_streamReader.EndOfStream ||
                currentLine.StartsWith(CalendarConstants.BEGIN_TIMEZONE) ||
                currentLine.StartsWith(CalendarConstants.BEGIN_APPOINTMENT)))
            {
                currentLine = _streamReader.ReadLine();
            }

            return currentLine.StartsWith(CalendarConstants.BEGIN_TIMEZONE);
        }

        private void MoveToNextEvent()
        {
            while (!(_streamReader.EndOfStream || _streamReader.ReadLine().StartsWith(CalendarConstants.BEGIN_APPOINTMENT)))
            {
            }
        }

        private void GetUTCOffset()
        {
            string currentLine = _streamReader.ReadLine();
            string offsetString1 = "";
            string offsetString2 = "";

            while (string.IsNullOrEmpty(offsetString1) || string.IsNullOrEmpty(offsetString2))
            {
                string[] parts = ParseFieldParts(currentLine);
                string offsetType = parts[0];
                string offset = parts[1];

                if (offsetType.StartsWith(CalendarConstants.FROM_OFFSET))
                {
                    offsetString1 = offset;
                }
                else if (offsetType.StartsWith(CalendarConstants.TO_OFFSET))
                {
                    offsetString2 = offset;
                }

                currentLine = _streamReader.ReadLine();
            }

            int offset1 = Int32.Parse(offsetString1) / 100;
            int offset2 = Int32.Parse(offsetString2) / 100;
            _utcOffset = offset1 < offset2 ?  offset1  : offset2;
        }

        private string GetNextLine()
        {
            string line = _streamReader.ReadLine();

            while (_streamReader.Peek() == CalendarConstants.ASCII_SPACE_CODE)
            {
                line = line + _streamReader.ReadLine().TrimStart(new char[] { ' ' });
            }

            return line;
        }

        private void ProcessLine(string line, Appointment appt)
        {
            string[] fieldParts = ParseFieldParts(line);
            string fieldName = fieldParts[0];
            string fieldValue = fieldParts[1];

            switch (fieldName)
            {
                case CalendarConstants.SUMMARY:
                    appt.Summary = ParserHelper.RemoveEscapeChars(fieldValue);
                    break;

                case CalendarConstants.LOCATION:
                    appt.Location = ParserHelper.RemoveEscapeChars(fieldValue);
                    break;

                case CalendarConstants.UID:
                    appt.SetUID(new Guid(fieldValue));
                    break;

                case CalendarConstants.URL:
                    // this gets rid of the quotes at either end
                    string url = fieldValue.Remove(0, 1);
                    url = url.Remove(url.Length - 1);
                    appt.URL = url;
                    break;

                case CalendarConstants.ATTENDEE:
                    appt.Attendees.Add(ParseAttendee(fieldValue));
                    break;

                case CalendarConstants.BEGIN:
                    appt.Reminder = ParseAlarm(appt.StartDateTime);
                    break;

                case CalendarConstants.TIMESTAMP:
                    appt.Timestamp = DateParser.ParseUTC(fieldValue);
                    break;

                case CalendarConstants.START_DATE_TIME:
                    appt.StartDateTime = ParseLocalDateTime(fieldValue);

                    if (fieldValue.Contains(CalendarConstants.DATE_VALUE))
                    {
                        appt.IsAllDayEvent = true;
                    }

                    break;

                case CalendarConstants.END_DATE_TIME:
                    appt.EndDateTime = ParseLocalDateTime(fieldValue);
                    break;

                case CalendarConstants.RECURRENCE_RULE:
                    appt.RecurrenceRule = ParseRecurrenceRule(fieldValue, CompareUTCToLocal(appt));
                    break;

                case CalendarConstants.NOTES:
                    appt.Notes = ParserHelper.RemoveEscapeChars(fieldValue);
                    break;

                case CalendarConstants.OCCURRENCE:
                    appt.Occurrence = ParseLocalDateTime(fieldValue);
                    break;

                case CalendarConstants.SEQUENCE_NUMBER:
                    appt.SequenceNumber = Int32.Parse(fieldValue);
                    appt.IsPartOfSeries = true;
                    break;
                    
                default:
                    appt.ExtraProperties.Add(line);
                    break;
            }
        }

        private string[] ParseFieldParts(string line)
        {
            int colonPos = line.IndexOfAny(new char[] {':', ';'});
            return new string[] { line.Substring(0, colonPos),
                                     line.Substring(colonPos + 1, line.Length - colonPos - 1) };
        }

        private Attendee ParseAttendee(string line)
        {
            Attendee attendee = new Attendee();
            attendee.Name = ParserHelper.GetAttendeeName(line);
            attendee.EmailAddress = ParserHelper.GetEmailAddress(line);

            return attendee;
        }

        private Reminder ParseAlarm(DateTime startDateTime)
        {
            Reminder reminder = new Reminder();
            string currentLine = _streamReader.ReadLine();

            while (!currentLine.StartsWith(CalendarConstants.END_REMINDER))
            {
                string[] fieldParts = ParseFieldParts(currentLine);
                string fieldName = fieldParts[0];
                string fieldValue = fieldParts[1];

                switch(fieldName)
                {
                    case CalendarConstants.TRIGGER:
                        ProcessTrigger(fieldValue, reminder);
                        break;
                    case CalendarConstants.REMINDER_UID:
                        reminder.SetUID(new Guid(fieldValue));
                        break;
                    default:
                        reminder.ExtraProperties.Add(currentLine);
                        break;
                }

                currentLine = _streamReader.ReadLine();
            }

            return reminder;
        }

        private DateTime ParseLocalDateTime(string line)
        {
            string[] parts = line.Split(new char[] { '=', ':' });

            if (parts.Length == 1)
            {
                return DateParser.ParseUTC(parts[0]);
            }

            if (parts[0].Equals(CalendarConstants.TIME_ZONE_ID))
            {
                return DateParser.ParseLocalTime(parts[2], _utcOffset);
            }

            return DateParser.ParseDate(parts[2]);
        }

        private RecurrenceRule ParseRecurrenceRule(string line, int UTCLocalDiff)
        {
            string[] properties = line.Split(';');
            RecurrenceRule recRule = new RecurrenceRule();

            foreach (string property in properties)
            {
                string[] propertyParts = property.Split('=');
                string propertyName = propertyParts[0];
                string propertyValue = propertyParts[1];

                switch (propertyName)
                {
                    case CalendarConstants.FREQUENCY:
                        recRule.Frequency = ParseFrequency(propertyValue);
                        break;
                    case CalendarConstants.COUNT:
                        recRule.Count = Int32.Parse(propertyValue);
                        break;
                    case CalendarConstants.RECURRENCE_END_DATE_TIME:
                        recRule.EndDateTime = DateParser.ParseUTC(propertyValue);
                        break;
                    case CalendarConstants.INTERVAL:
                        recRule.Interval = Int32.Parse(propertyValue);
                        break;
                    case CalendarConstants.MONTH_WEEKDAY:
                        string[] monthWeekdayStrings = propertyValue.Split(',');

                        foreach (string monthWeekdayString in monthWeekdayStrings)
                        {
                            MonthWeekday monthWeekday = ParseMonthWeekday(monthWeekdayString);
                            Weekday weekday = monthWeekday.Weekday;

                            if (UTCLocalDiff < 0)
                            {
                                weekday = Decrement(weekday);
                            }
                            else if (UTCLocalDiff > 0)
                            {
                                weekday = Increment(weekday);
                            }

                            monthWeekday.Weekday = weekday;
                            recRule.MonthWeekdays.Add(monthWeekday);
                        }

                        break;
                    case CalendarConstants.MONTH_DATE:
                        recRule.MonthDate = Int32.Parse(propertyValue);
                        break;
                    default:
                        recRule.ExtraProperties.Add(property);
                        break;
                }
            }

            return recRule;
        }
        
        private MonthWeekday ParseMonthWeekday(string line)
        {
            string[] parts = ParserHelper.GetMonthWeekdayParts(line);

            MonthWeekday monthWeekday = new MonthWeekday();

            string weekNumberString = parts[0];
            int weekNumber = 0;

            if (!weekNumberString.Equals(string.Empty))
            {
                weekNumber = Int32.Parse(weekNumberString);
            }

            monthWeekday.WeekNumber = weekNumber;
            monthWeekday.Weekday = ParseWeekday(parts[1]);
            return monthWeekday;
        }

        private Frequency ParseFrequency(string freqString)
        {
            switch (freqString)
            {
                case "SECONDLY":
                    return Frequency.Secondly;

                case "MINUTELY":
                    return Frequency.Minutely;

                case "HOURLY":
                    return Frequency.Hourly;

                case "DAILY":
                    return Frequency.Daily;

                case "WEEKLY":
                    return Frequency.Weekly;

                case "MONTHLY":
                    return Frequency.Monthly;

                case "YEARLY":
                    return Frequency.Yearly;

                default:
                    throw new ArgumentException(
                        "Frequency string must be one of: SECONDLY, MINUTELY,"
                        + "HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY");
            }
        }

        private Weekday ParseWeekday(string weekdayString)
        {
            switch (weekdayString)
            {
                case "SU":
                    return Weekday.Sunday;
                case "MO":
                    return Weekday.Monday;
                case "TU":
                    return Weekday.Tuesday;
                case "WE":
                    return Weekday.Wednesday;
                case "TH":
                    return Weekday.Thursday;
                case "FR":
                    return Weekday.Friday;
                case "SA":
                    return Weekday.Saturday;
                default:
                    throw new ArgumentException(
                        "Weekday string must be one of: SU, MO, TU, WE, " +
                        "TH, FR, SA");
            }
        }

        private void ProcessTrigger(string line, Reminder reminder)
        {
            if (line.StartsWith(CalendarConstants.DATE_TIME_VALUE))
            {
                reminder.OnDateTrigger = DateParser.ParseUTC(ParseFieldParts(line)[1]);
                return;
            }
            
            string durationString = line.Substring(line.IndexOf('P') + 1);

            if (string.IsNullOrEmpty(durationString))
            {
                reminder.Trigger = new TimeSpan(0);
                return;
            }

            if (durationString[0] == 'T')
            {
                durationString = durationString.Substring(1);
            }

            string[] parts = ParserHelper.GetDurationParts(durationString);
            int number = -1 * Int32.Parse(parts[0]);
            char incrementType = parts[1][0];

            switch (incrementType)
            {
                case 'W':
                    reminder.Trigger = new TimeSpan(number * 7, 0, 0, 0);
                    return;
                case 'D':
                    reminder.Trigger = new TimeSpan(number, 0, 0, 0);
                    return;
                case 'H':
                    reminder.Trigger = new TimeSpan(number, 0, 0);
                    return;
                case 'M':
                    reminder.Trigger = new TimeSpan(0, number, 0);
                    return;
                case 'S':
                    reminder.Trigger = new TimeSpan(0, 0, number);
                    return;
                default:
                    throw new ArgumentException("Increment type must be one of W, D, H, M, S");
            }
        }

        private void CombineOccurrences(List<Appointment> appts)
        {
            Dictionary<Guid, Appointment> seriesEvents = new Dictionary<Guid, Appointment>();

            foreach (Appointment appt in appts)
            {
                if (appt.IsPartOfSeries && appt.SequenceNumber == 0)
                {
                    seriesEvents.Add(appt.UID, appt);
                }
            }

            List<Appointment> apptsToRemove = new List<Appointment>();

            foreach (Appointment appt in appts)
            {
                if (appt.IsPartOfSeries && appt.SequenceNumber == 1)
                {
                    apptsToRemove.Add(appt);
                    seriesEvents[appt.UID].Occurrences.Add(appt);
                }
            }

            foreach (Appointment appt in apptsToRemove)
            {
                appts.Remove(appt);
            }
        }

        private int CompareUTCToLocal(Appointment appt)
        {
            int offset = _utcOffset + (DateTime.SpecifyKind(appt.StartDateTime, DateTimeKind.Unspecified).IsDaylightSavingTime() ? 1 : 0);
            return appt.StartDateTime.Date.CompareTo(
                (appt.StartDateTime + new TimeSpan(offset, 0, 0)).Date);
        }

        private Weekday Increment(Weekday weekday)
        {
            int newNumber = (((int)weekday) + 1) % 7;
            return (Weekday)newNumber;
        }

        private Weekday Decrement(Weekday weekday)
        {
            int newNumber = (((int)weekday) + 6) % 7;
            return (Weekday)newNumber;
        }

        #endregion
    }
}